-
Notifications
You must be signed in to change notification settings - Fork 14k
rust-installer/install-template.sh: improve efficiency, step 1. #145809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This round replaces repetitive pattern matching in the inner loop of this script using grep (which causes a fork() for each test) with built-in pattern matching in the bourne shell using the case / esac construct. This in reference to rust-lang#80684 and is a separated-out request from rust-lang/rust-installer#111 which apparently never got any review. The forthcoming planned "step 2" change builds on top of this change, and replaces the inner-loops needless uses of sed (which again causes a fork() for each instance) with the suffix removal constructs from the bourne shell. Since this change touches lots of the same lines this change does, that pull request cannot be submitted before this one is accepted. Hopefully this first step is less controversial than the latter change.
|
rustbot has assigned @Mark-Simulacrum. Use |
|
Hi, thanks for the PR! Did you try to run any benchmarks (i.e. install a tarball component before/after) the change? |
| bin/*) | ||
| run cp "$_src_dir/$_component/$_file" "$_file_install_path" | ||
| run chmod 755 "$_file_install_path" | ||
| ;; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a useful change of behavior, but you did not mention it in your PR summary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am a little uncertain what "this" refers to. There was no intention to change the behavior of the script. Note that the test in the previous "if" had an "or", so both tests needs to be covered going forward, and that is what I think the new code does. The case construct can only cover the test for the path name, and cannot test for the executeable-ness of the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah right, I misread. Still I don't understand why these couldn't be combined like in the original code, perhaps by first testing for "bin"ness and saving the result in a variable, then doing the if-or?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original code contains 2 copies of "run cp ...; run chmod ...", your code contains 3 copies, but ideally we'd have a single copy...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The two latest commits should have tidied up this issue, there is now just one "run cp ..." command in the pointed-to spot.
Admittedly no. However, this set of modifications came about because this script was absurdly slow when doing the builds / install during my testing of the various rust releases up through the ages, on & for the various different NetBSD targets. To run a benchmark I would have to figure out how to rig up a test scaffolding for this script, since just doing this as part of a full build will take way too long time. To me it is sort of obvious that doing a massive number of avoidable The question can of course come from a couple of different perspectives:
I am hoping the question doesn't come from the third perspective. The second perspective also should not be a valid objection; as the referenced issue describes, it is well known that this script in its current form is just slow. |
|
@bors try Let's produce some artifacts -- I think the install scripts aren't exercised anywhere(?) in our normal release process, so it'll need some manual testing. |
rust-installer/install-template.sh: improve efficiency, step 1.
This comment has been minimized.
This comment has been minimized.
|
It's a larger change, but I'm also wondering why this was written as a shell script. AFAICT, this is always bundled into a tarball we produce that contains some kind of binary artifacts. Maybe this could be a Rust program? That would (a) improve our ability to review changes and (b) likely help with performance, at least insofar as we can avoid any sub-shells that aren't needed. |
|
OK, I've done some testing. First off, I install rust 1.88.0 into an empty prefix via
to collect system call trace to see how many
With the original install.sh script, we get 3910 Secondly, doing the install into an already-populated prefix gets us with the original installer the following outputs from "time" in csh: With the suggested "case / esac for pattern matching" version suggested here, I got So ... a nearly 30% reduction in wallclock time on this particular host (which is pretty beefy, I predict that the effect will be even more pronounced on slower hosts), and a reduction in the number of And ... the reason this is particularly noticeable with the "doc" component is most probably that it has a rather larger set of files. The components installed here have relatively few: I don't have a build with the doc component at hand at the moment. |
Yes, that would be a larger change. Why it is the way it is (a shell script), I cannot comment on at the moment. And turning this into a rust program would exceed my current rust abilities, so it would not come from this corner. However, let me suggest that we first measure the performance improvements we can get with "known fixes" to the existing script to get this from "unbearably slow" to "manageable also on slow hosts" before embarking on that larger rewrite. Expediency has to count for something... And... It also looks like this avenue has been attempted before, #80684 contains pointers to both similar suggestions (which were not taken), and some which were (the --bulk-dirs for docs). At the end of this it might be worth reviewing those other old suggestions to see which ones are still applicable. |
|
And ... to preview the suggested next pull request, replacing cut and sed in the inner loop of the script with parameter expansion which does "remove largest suffix pattern" and "remove shortest prefix pattern" modifications reduces the number of So, average wall-clock time of 4.62, which is around 35% of the original timing (reduced by 65%), and the number of |
|
@rust-lang/bootstrap anyone interested in reviewing the changes in this (and future) PRs? I'd personally rather invest the time spent on reviewing complicated shell code into writing a Rust replacement (unless someone finds reasons not to do that). |
|
I don't know bash much and don't feel confident reviewing this, tbh. By a Rust replacement, you mean writing a Rust crate that will perform the installation process, compiling it for the host target, and then shipping that compiled installed binary in the tarballs, instead of install.sh? |
|
Yes. I guess it might be hard due to being for a particular host target (many of our tarballs don't have a particular host target). But we could either have lots of binaries (like rustup) dispatched via the shell script or universal binaries (though that seems plausibly hard, not sure). |
I think the issue with making it a rust program is that you can install a component for a different target than the host, which doesn't work if the tarball contains an installer for a single target. Especially for the rust-std components it is essential that installing it for a different target works to be able to do cross-compilation. |
|
So ... how do we progress this pull request? Yes, I realize that Bourne Shell fluency would be a requirement to OK this change. |
|
So... Is there any way to move forward with this pull request / review? |
|
Asked on Zulip if there's anyone up for reviewing. |
|
My comment from Aug 24 still seems unaddressed... |
| else | ||
| run cp "$_src_dir/$_component/$_file" "$_file_install_path" | ||
| run chmod 644 "$_file_install_path" | ||
| case "$_file" in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel that hiring case statement only for one pattern is too much. How about using a flag?
local _is_bin=0
case "$_file" in
etc/*)
local _f="$(echo "$_file" | sed 's/^etc\///')"
_file_install_path="$CFG_SYSCONFDIR/$_f"
;;
bin/*)
local _f="$(echo "$_file" | sed 's/^bin\///')"
_file_install_path="$CFG_BINDIR/$_f"
_is_bin=1 # Turn the flag on
;;
# ....
esac
# ...
if [ $_is_bin -eq 1 ] || test -x "$_src_dir/$_component/$_file"; then # use the flag here
run cp "$_src_dir/$_component/$_file" "$_file_install_path"
run chmod 755 "$_file_install_path"
else
run cp "$_src_dir/$_component/$_file" "$_file_install_path"
run chmod 644 "$_file_install_path"There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like that should be doable. Stylistically I would instead of 0 and 1 use "true" and "false", since I can then drop the "test" via [. The file-copying could also be moved outside of the test. However...
case / esac are shell built-ins, and are therefore comparatively cheap, as they do not require a new process be fork()ed. Using grep for the pattern matching, as the original code did is what's expensive...
Let's see if I can draft something along the lines suggested.
...now done.
| bin/*) | ||
| run cp "$_src_dir/$_component/$_file" "$_file_install_path" | ||
| run chmod 755 "$_file_install_path" | ||
| ;; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original code contains 2 copies of "run cp ...; run chmod ...", your code contains 3 copies, but ideally we'd have a single copy...
Also factor out common commands to outside of test.
|
This comment has been minimized.
This comment has been minimized.
| local _f="$(echo "$_file" | sed 's/^etc\///')" | ||
| _file_install_path="$CFG_SYSCONFDIR/$_f" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is Bash, we also don't need to use sed to cut off the front of this string, but can use ${_file#*/} to cut off the first part up to and including the first slash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I know. This isn't just because this is Bash, it is because it's a POSIX Bourne Shell (which also has that construct). I have a slew of similar changes lined up as "step 2" of the efficiency improvements for this script. So once this pull request gets accepted & applied (I hope that will happen...), a new pull request will follow. Ref. my comments about performance testing above. But since my earlier attempt at getting this all accepted stalled or was ignored, I decided it would be tactically better to try to divide this up in bite-size changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate that, and I see that your (much) earlier attempt did include such changes. I'm not sure the solution is dribbling things out though, especially when related changes get brought up in review. It feels like a waste of my review efforts. Perhaps you could reopen a separate PR with all the changes you had lined up before regarding speeding up this particular script with all my comments to this one addressed? I feel I'm more likely to approve such a PR (not that I have any formal power to do that, but it may count for something...). Given that lack of bash expertise is one of the reasons given for the slow review I'd also recommend commenting all these constructs.
| share/man/*) | ||
| local _f="$(echo "$_file" | sed 's/^share\/man\///')" | ||
| _file_install_path="$CFG_MANDIR/$_f" | ||
| ;; | ||
| share/doc/*) | ||
| # HACK: Try to support overriding --docdir. Paths with the form | ||
| # "share/doc/$product/" can be redirected to a single --docdir | ||
| # path. If the following detects that --docdir has been specified | ||
| # then it will replace everything preceding the "$product" path | ||
| # component. The problem here is that the combined rust installer | ||
| # contains two "products": rust and cargo; so the contents of those | ||
| # directories will both be dumped into the same directory; and the | ||
| # contents of those directories are _not_ disjoint. Since this feature | ||
| # is almost entirely to support 'make install' anyway I don't expect | ||
| # this problem to be a big deal in practice. | ||
| if [ "$CFG_DOCDIR" != "<default>" ] | ||
| then | ||
| local _f="$(echo "$_file" | sed 's/^share\/doc\/[^/]*\///')" | ||
| _file_install_path="$CFG_DOCDIR/$_f" | ||
| fi | ||
| ;; | ||
| share/*) | ||
| local _f="$(echo "$_file" | sed 's/^share\///')" | ||
| _file_install_path="$CFG_DATADIR/$_f" | ||
| ;; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could use a nested case statement: after matching "share/" remove it via ${_file#*/}, then match on the remainder. (This would also prevent repeated matching of "share/".)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no danger of "multiple matches". The man page for sh(1) on NetBSD says:
The syntax of the case command is
case word in
[(] pattern) [list] ;&
[(] pattern) [list] ;;
...
esac
The pattern can actually be one or more patterns (see Shell Patterns
described later), separated by "|" characters.
word is expanded and matched against each pattern in turn, from first to
last, with each pattern being expanded just before the match is
attempted. When a match is found, pattern comparisons cease, and the
associated list, if given, is evaluated. If the list is terminated with
";&" execution then falls through to the following list, if any, without
evaluating its pattern, or attempting a match. When a list terminated
with ";;" has been executed, or when esac is reached, execution of the
case statement is complete. The exit status is that of the last command
executed from the last list evaluated, if any, or zero otherwise.
and I am pretty certain that this is identical to what POSIX specifies. So there should be no need to further complicate this construct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not talking about danger; this is just another optimization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is ... unproven that this makes much of a difference performance-wise. Can you benchmark what difference it would make?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If for some reason, you have a preference for this code to remain written in a way in which it quite obviously does more work than necessary, and have benchmarks that show that it matters only negligibly, then I'll grudgingly accept that, but it still wouldn't be a very good reason not to write it the efficient way. The efficient way is also not any more complex, so I can't see any downsides. Can you?
| then | ||
| run cp "$_src_dir/$_component/$_file" "$_file_install_path" | ||
| run chmod 755 "$_file_install_path" | ||
| if $_is_bin || test -x "$_src_dir/$_component/$_file"; then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're testing whether files are executable to decide the mod bits to set, then why do we also need to check whether they are in the bin/ directory? Are there perhaps some files in the bin/ directory that are missing the executable bit, and we should instead add those missing bits?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am just trying to faithfully 1:1 replicate the effect of what the original code does, and it has this test. If this should be changed, I would posit that is a separate change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if it should be changed, but since I'm noticing this as part of my review I thought I'd bring it up as something that doesn't seem right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I cannot vouch for all the install components thrown at this script. I am merely trying to faithfully reproduce the actions the script already did before this suggested change.
This round replaces repetitive pattern matching in the inner loop of this script using grep (which causes a
fork()for each test) with built-in pattern matching in the Bourne shell using thecase/esacconstruct.This in reference to
#80684
and is a separated-out request from
rust-lang/rust-installer#111
which apparently never got any review.
The forthcoming planned "step 2" change builds on top of this change, and replaces the inner-loops needless uses of
sed(which again causes afork()for each instance) with the suffix removal constructs from the Bourne shell. Since this change touches lots of the same lines this change does, that pull request cannot be submitted before this one is accepted.Hopefully this first step is less controversial than the latter change.